package serverClient;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

public class StateController {

	private final boolean DEBUG = false;

	// Version number
	private String version;
	// Config directory
	private String confDir;

	private StObserverController observers;

	// Initiates variables
	private ServerSocket listenSocket = null;
	private Socket stateSocket = null;
	private Socket secondSocket = null;
	private PrintWriter out = null;
	private BufferedReader in = null;
	private Thread listenThread;
	private Thread localThread1;
	private Thread localThread2;
	private File curDir;
	private File curDir2;
	private String ip;
	private int sendPort;
	private int listenPort;
	private boolean startListen;
	private boolean localSync;
	private boolean useExactTime;

	private Server tServer = null;
	private Client tClient = null;
	private Server tServer2 = null;
	private Client tClient2 = null;

	// 3 syncModes: dual, out, in
	private String syncMode = "dual";
	// States to know if the client should delete any files or not
	private boolean allowDelete;
	private boolean deleteWarning;
	private boolean includeHidden;

	// Is set if sync button is pressed (or GUI timer is running), then a sync message is to be sent
	// Is also set to indicate that the exception of closed listen socket is controlled
	private boolean sendSync = false;
	// If this is set the listenThread will only run methods that will reset sockets etc.
	private boolean resetThread = false;
	// Has to be set when we want to finish running the thread
	private boolean exitThread = false;
	// If this is set the method's in the local sync part will be skipped
	private boolean resetLocalThreads = false;
	// Is set when a sync is running (either remote or local)
	private boolean runningStateController = false;
	// Boolean to know if a local sync is currently running
	private boolean runningLocalSync = false;


	// Constructor
	public StateController(String version, String confDir) {

		this.version = version;
		this.confDir = confDir + File.separator;
		observers = new StObserverController();
	}


	// These set-method's are called from App()
	public void setDir(String curDir) {
		this.curDir = new File(curDir);
	}

	public void setDir2(String curDir2) {
		this.curDir2 = new File(curDir2);
	}

	public void setIP(String ip) {
		this.ip = ip;
	}

	public void setSendPort(int sendPort) {
		this.sendPort = sendPort;
	}

	public void setListenPort(int listenPort) {
		this.listenPort = listenPort;
		if(startListen) {
			observers.sendConnectionMessage("Connection: Listening on port " + this.listenPort);
		}
	}

	public void setStartListen(boolean startListen) {
		this.startListen = startListen;
	}

	public void setLocalSync(boolean localSync) {
		this.localSync = localSync;
		connectionMessage(localSync);
	}

	public void setSyncMode(String syncMode) {
		this.syncMode = syncMode;
	}

	public void setAllowDelete(boolean allowDelete) {
		this.allowDelete = allowDelete;
	}

	public void setDeleteWarning(boolean deleteWarning) {
		this.deleteWarning = deleteWarning;
	}

	public void setIncludeHidden(boolean includeHidden) {
		this.includeHidden = includeHidden;
	}

	public void setUseExactTime(boolean useExactTime) {
		this.useExactTime = useExactTime;
	}

	// Functions for the observers //
	public void addLogObserver(LogObserver o) {
		observers.addLogObserver(o);
	}
	
	public void addGuiChangeObserver(GuiChangeObserver o) {
		observers.addGuiChangeObserver(o);
	}
	
	public void addConfigChangeObserver(ConfigChangeObserver o) {
		observers.addConfigChangeObserver(o);
	}
	
	

	// Starts a thread and calls the needed methods in a loop
	public void startStateController() {
		if(!runningLocalSync && startListen) {
			listenThread = new Thread(new Runnable() {
				public void run() {
					exitThread = false; // So the thread will run next time it is called
					while(!exitThread && startListen) {
						observers.enableGuiComponents(true); // Unlocks all components in the GUI
						initServerSocket();
						runStateController();
						closeSockets();
						if(!runningLocalSync) { // Run this only if a local sync is not running
							resetThreadVariables();
						}
					}
					observers.enableGuiComponents(true); // Unlocks all components in the GUI
					if(DEBUG) System.out.println("ListenThread finished!");
				}
			});
			listenThread.start();
			observers.sendConnectionMessage("Connection: Listening on port " + listenPort);
		}
	}

	// Binds the listenSocket to a port 
	private void initServerSocket() {
		if(DEBUG) System.out.println("Starting to listen on: " + listenPort);

		try {
			listenSocket = new ServerSocket(listenPort);
		} catch (IOException e) {
			resetThread = true;
			exitThread = true;
			startListen = false;
			observers.sendConnectionWarning("Could not listen on port: " + listenPort);
			if(DEBUG) System.err.println("Could not listen on port: " + listenPort);

		}
	}

	// Run-list for the thread after initialization of listenSocket 
	private void runStateController() {

		if (!resetThread) waitForAccept();
		if (!resetThread) initInOut();
		if (!resetThread && sendSync) {
			out.println("Sync:" + version + ":" + syncMode + ";" + includeHidden + ";" + useExactTime);
		}
		if (!resetThread) waitingForMsg();
		if (!resetThread) {
			observers.sendConnectionMessage("Connection: Listening on port: " + this.listenPort);
		}
	}

	private void waitForAccept() {
		try {
			if(DEBUG) System.out.println("Waiting for accept...");
			stateSocket = null;
			stateSocket = listenSocket.accept();
			if(runningStateController) { // Checks if stateController is already running
				resetThread = true;
				if(DEBUG) System.out.println("StateController is already running!");
			}
			runningStateController = true;
			// If socket got remotely connected
			observers.enableGuiComponents(false); // Locks all components in the GUI
			ip = stateSocket.getInetAddress().getHostAddress();
			observers.sendConnectionMessage("Incomming connection: " + ip);
		} catch (IOException e) {
			if(sendSync) { // If we initiates the connection with the sync button
				runningStateController = true;
				observers.enableGuiComponents(false); // Locks all components in the GUI
				if(DEBUG) System.out.println("Socket manually closed");
			}
			else { // If we close the socket or anything else goes wrong
				resetThread = true;
				if(DEBUG) System.err.println("Accept failed.");
			}
		}
	}

	// Binds the in and out-streams to the stateSocket
	private void initInOut() {

		try {

			// Locks until stateSocket is bound
			if(DEBUG) System.out.println("...waiting for socket to be bound");
			while (stateSocket == null || stateSocket.isClosed()) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				if (resetThread) break;
			}

			// Initiates in and out when stateSocket is bound
			if (!resetThread) {
				out = new PrintWriter(stateSocket.getOutputStream(), true);
				in = new BufferedReader(new InputStreamReader(stateSocket.getInputStream()));
			}
		} catch (IOException e) {
			resetThread = true;
			e.printStackTrace();
		}
	}

	// Handles incoming messages and starts the server or client as directed
	private void waitingForMsg() {
		if (!resetThread) {
			String msg = null;
			if(DEBUG) System.out.println("waitingForMsg...");
			try {
				if( ( msg = in.readLine() ) != null) {

					// If we receive a sync message
					if(msg.startsWith("Sync")) {
						String[] syncSplit = msg.split(":");
						if(correctVersion(syncSplit[1])) { // Controlling version
							changeConfigSettings(syncSplit[2]); // Sets our syncMode, includeHidden status, and useExactTime status
							if(DEBUG) System.out.println("SyncMode: " + syncMode);
							if(DEBUG) System.out.println("HiddenStatus: " + includeHidden);
							if(DEBUG) System.out.println("UseExactTime: " + useExactTime);
							out.println("Okej");

							// Starts the sync passively
							BufferedInputStream inBufStream = new BufferedInputStream(stateSocket.getInputStream());
							BufferedOutputStream outBufStream = new BufferedOutputStream(stateSocket.getOutputStream());
							passiveStart(inBufStream, outBufStream);
							outBufStream.close();
							inBufStream.close();
						}
						else {
							out.println("WrongVersion");
							observers.sendConnectionWarning("Wrong version!");
							resetThread = true;
						}
					}

					// If we receive the ACK message "Okej"
					if(msg.equals("Okej") && sendSync) {

						// Starts the sync actively
						BufferedInputStream inBufStream = new BufferedInputStream(stateSocket.getInputStream());
						BufferedOutputStream outBufStream = new BufferedOutputStream(stateSocket.getOutputStream());
						activeStart(inBufStream, outBufStream);
						outBufStream.close();
						inBufStream.close();
					}

					// If we receive the message "WrongVersion"
					if(msg.equals("WrongVersion")) {
						observers.sendConnectionWarning("Wrong version!");
						resetThread = true;
					}

				}
			} catch (Exception e) {
				e.printStackTrace();
				if(resetThread) {
					observers.sendConnectionWarning("Connection failed!");
				}
			}
		}
	}

	private boolean correctVersion(String verStr) {
		return version.equals(verStr);
	}

	// Figures out our syncMode, includeHidden status, and useExactTime status depending on the remote's configurations
	private void changeConfigSettings(String changes) {

		String[] change = changes.split(";");

		// Handle sync mode change
		if(change[0].equals("dual")) {
			syncMode = "dual";
		}
		else if(change[0].equals("out")) {
			syncMode = "in";
		}
		else if(change[0].equals("in")) {
			syncMode = "out";
		}
		else {
			syncMode = "backup";
		}

		// Handle hidden mode change
		includeHidden = Boolean.parseBoolean(change[1]);

		// Handle if we should use exact time or not
		useExactTime = Boolean.parseBoolean(change[2]);
		
		observers.sendChangeConfigSyncMode(syncMode);
		observers.sendChangeConfigIncludeHidden(includeHidden);
		observers.sendChangeConfigUseExactTime(useExactTime);
	}

	// Help functions for passive- and acktiveStart
	// Uses the right boolean depending on if the current run is local or not
	private boolean getResetStatus() {
		if(localSync) return resetLocalThreads;
		else return resetThread;
	}
	private void setResetStatus(boolean reset) {
		if(localSync) resetLocalThreads = reset;
		else resetThread = reset;
	}

	private void passiveStart(BufferedInputStream inBufStream, BufferedOutputStream outBufStream) {

		if(syncMode.equals("dual")) { // Dual way
			tServer = new Server(confDir, inBufStream, outBufStream, curDir, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
			observers.addLogObserverToServer(tServer);
			observers.addGuiChangeObserverToServer(tServer);
			tServer.doNotPrintFinalSyncCheck();
			if (!tServer.runServer()) {
				resetThread = true;
			}
			if(DEBUG) System.out.println("Finished Server");

			if(!resetThread) {
				tClient = new Client(confDir, inBufStream, outBufStream, curDir, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
				observers.addLogObserverToClient(tClient);
				observers.addGuiChangeObserverToClient(tClient);
				if (!tClient.runClient()) {
					resetThread = true;
				}
				if(DEBUG) System.out.println("Finished Client");
			}
		}

		else if(syncMode.equals("out")) { // Out going
			tClient = new Client(confDir, inBufStream, outBufStream, curDir, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
			observers.addLogObserverToClient(tClient);
			observers.addGuiChangeObserverToClient(tClient);
			if (!tClient.runClient()) {
				resetThread = true;
			}
			if(DEBUG) System.out.println("Finished Client");
		}

		else if(syncMode.equals("in") || syncMode.equals("backup")) { // In coming or Backup mode
			tServer = new Server(confDir, inBufStream, outBufStream, curDir, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
			observers.addLogObserverToServer(tServer);
			observers.addGuiChangeObserverToServer(tServer);
			if (!tServer.runServer()) {
				resetThread = true;
			}
			if(DEBUG) System.out.println("Finished Server");
		}
	}

	private void activeStart(BufferedInputStream inBufStream, BufferedOutputStream outBufStream) {

		if(syncMode.equals("dual")) { // Dual way
			tClient = new Client(confDir, inBufStream, outBufStream, curDir, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
			observers.addLogObserverToClient(tClient);
			observers.addGuiChangeObserverToClient(tClient);
			tClient.doNotPrintFinalSyncCheck();
			if(!tClient.runClient()) {
				setResetStatus(true);
			}
			if(DEBUG) System.out.println("Finished Client");

			if(!getResetStatus()) {
				tServer = new Server(confDir, inBufStream, outBufStream, curDir, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
				observers.addLogObserverToServer(tServer);
				observers.addGuiChangeObserverToServer(tServer);
				if (!tServer.runServer()) {
					setResetStatus(true);
				}
				if(DEBUG) System.out.println("Finished Server");
			}
		}

		else if(syncMode.equals("out") || syncMode.equals("backup")) { // Out going or Backup mode
			tClient = new Client(confDir, inBufStream, outBufStream, curDir, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
			observers.addLogObserverToClient(tClient);
			observers.addGuiChangeObserverToClient(tClient);
			if (!tClient.runClient()) {
				setResetStatus(true);
			}
			if(DEBUG) System.out.println("Finished Client");
		}

		else if(syncMode.equals("in")) { // In coming
			tServer = new Server(confDir, inBufStream, outBufStream, curDir, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
			observers.addLogObserverToServer(tServer);
			observers.addGuiChangeObserverToServer(tServer);
			if (!tServer.runServer()) {
				setResetStatus(true);
			}
			if(DEBUG) System.out.println("Finished Server");
		}
	}

	// Starts the sync session for the second folder
	private void secondFolderStart(BufferedInputStream inBufStream2, BufferedOutputStream outBufStream2) {

		if(syncMode.equals("dual")) { // Dual way
			tServer2 = new Server(confDir, inBufStream2, outBufStream2, curDir2, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
			observers.addLogObserverToServer(tServer2);
			tServer2.setSecondSyncThread(true);
			tServer2.useSecondLocalDir();
			if (!tServer2.runServer()) {
				resetLocalThreads = true;
			}

			if (!resetLocalThreads) {
				tClient2 = new Client(confDir, inBufStream2, outBufStream2, curDir2, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
				observers.addLogObserverToClient(tClient2);
				tClient2.setSecondSyncThread(true);
				tClient2.useSecondLocalDir();
				if (!tClient2.runClient()) {
					resetLocalThreads = true;
				}
			}
		}

		else if(syncMode.equals("out") || syncMode.equals("backup")) { // Out going or Backup mode
			tServer2 = new Server(confDir, inBufStream2, outBufStream2, curDir2, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
			observers.addLogObserverToServer(tServer2);
			tServer2.setSecondSyncThread(true);
			tServer2.useSecondLocalDir();
			if (!tServer2.runServer()) {
				resetLocalThreads = true;
			}
		}

		else if(syncMode.equals("in")) { // In coming
			tClient2 = new Client(confDir, inBufStream2, outBufStream2, curDir2, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
			observers.addLogObserverToClient(tClient2);
			tClient2.setSecondSyncThread(true);
			tClient2.useSecondLocalDir();
			if (!tClient2.runClient()) {
				resetLocalThreads = true;
			}
		}
	}

	private void resetThreadVariables() {
		sendSync = false;
		resetThread = false;
		resetLocalThreads = false;
		runningStateController = false;
	}

	private void closeSockets() {
		try {
			if (in != null) in.close();
			if (out != null) out.close();
			if (tClient2 != null) tClient2.closeBuffs();
			if (tServer2 != null) tServer2.closeBuffs();
			if (tClient != null) tClient.closeBuffs();
			if (tServer != null) tServer.closeBuffs();
			if (secondSocket != null) secondSocket.close();
			if (stateSocket != null) stateSocket.close();
			if (listenSocket != null) listenSocket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// Sets the variables to make the thread start the client
	public void startClientSync(String sIP, int sPort, String curDir2, boolean locSync) {
		if(DEBUG) System.out.println("Running stateController: " + runningStateController);
		if(DEBUG) System.out.println("Exit thread: " + resetThread);
		if(DEBUG) System.out.println("Start listen: " + startListen);
		if(DEBUG) System.out.println("Local sync: " + locSync);
		if(DEBUG) System.out.println("Running Local Sync: " + runningLocalSync);

		if (!runningStateController) {
			runningStateController = true;
			ip = sIP;
			sendPort = sPort;
			this.curDir2 = new File(curDir2);
			this.localSync = locSync;


			class OpenSocket extends Thread {
				OpenSocket() {
				}

				public void run() {

					// LokalSync initiated
					if(localSync) {

						runningLocalSync = true;
						endListenThread();
						observers.enableGuiComponents(false); // Locks all GUI components
						initListenSocket(10);
						startLocalSync();
					}
					// Remote sync initiated
					else if(startListen) {

						sendSync = true;
						try {
							listenSocket.close();
						} catch (IOException e) {
							e.printStackTrace();
						}

						try {
							if(DEBUG) System.out.println("trying to open socket to: " + ip + ":" + sendPort);
							observers.sendConnectionMessage("Connecting to: " + ip + ":" + sendPort);
							stateSocket = new Socket(ip, sendPort);		
							observers.sendConnectionMessage("Connected to: " + ip + ":" + sendPort);
						} catch (UnknownHostException e) {
							observers.sendConnectionWarning("Unknown host: " + ip + ":" + sendPort);
							if(DEBUG) System.err.println("Can't find host: " + ip);
							resetListenThread();
						} catch (IOException e) {
							observers.sendConnectionWarning("Could not connect to: " + ip + ":" + sendPort);
							if(DEBUG) System.err.println("Couldn't get I/O for the connection to: " + ip + ":" + sendPort);
							resetListenThread();
						}
					}
				}

			}
			OpenSocket openSocketThread = new OpenSocket();
			openSocketThread.start();
		}
	}

	// Is called if startClientSync() fails
	private void resetListenThread() {
		if(listenThread != null && listenThread.isAlive()) {
			resetThread = true;
			sendSync = false;
			closeSockets();
		}
		else {
			resetThreadVariables();
		}
	}

	// Is called when the cancel button is pressed
	public void resetThread() {
		try {
			if(runningStateController) {
				resetThread = true;
				resetLocalThreads = true;
				if(DEBUG) System.out.println("Reseting threads!");
				if (in != null) in.close();
				if (out != null) out.close();
				if (tClient2 != null) tClient2.closeBuffs();
				if (tServer2 != null) tServer2.closeBuffs();
				if (tClient != null) tClient.closeBuffs();
				if (tServer != null) tServer.closeBuffs();
				if (secondSocket != null) secondSocket.close();
				if (stateSocket != null) stateSocket.close();
				if (listenSocket != null) listenSocket.close();

				// Locks until listenThread has finished TODO
				/*
				if(listenThread != null) {
					while( listenThread.isAlive() ) {
						listenThread.setPriority(Thread.MAX_PRIORITY);
						if(DEBUG) System.out.println("listenThread is still running!");
					}
					listenThread.setPriority(Thread.NORM_PRIORITY);
				}
				if(DEBUG) System.out.println("Reseting thread variables!");
				resetThreadVariables();
				 */
			}
			else {
				if(DEBUG) System.out.println("Reseting thread variables!");
				resetThreadVariables();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// Is called when startListen is unselected
	public void exit() {
		try {
			// Run this only if a local sync is not running
			if(!runningLocalSync) {
				exitThread = true;
				resetThread = true;
				sendSync = false;
				if(DEBUG) System.out.println("Exiting listenThread!");
				if (in != null) in.close();
				if (out != null) out.close();
				if (tClient != null) tClient.closeBuffs();
				if (tServer != null) tServer.closeBuffs();
				if (stateSocket != null) stateSocket.close();
				if (listenSocket != null) listenSocket.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}



	// Local Sync method's //
	public void connectionMessage(boolean on) {
		if(on) {
			observers.sendConnectionMessage("Connection: Local sync mode on");
		}
		else {
			observers.sendConnectionMessage("Connection: Local sync mode off");
		}
	}


	// Ends the listenThread if it is running
	private void endListenThread() {

		resetThread = true;
		exitThread = true;
		try {
			if(listenSocket != null) listenSocket.close();
		} catch (IOException e) {
			resetLocalThreads = true;
			e.printStackTrace();
		}
		// Locks until listenThread has finished
		if(listenThread != null) {
			while( listenThread.isAlive() ) {
				listenThread.setPriority(Thread.MAX_PRIORITY);
				if(DEBUG) System.out.println("listenThread is still running!");
				Thread.yield();
			}
			listenThread.setPriority(Thread.NORM_PRIORITY);
		}
	}

	// Initiates the listen socket
	// noOfTriesLeft if the amount of times it tries to initiate the listen socket
	public void initListenSocket(int noOfTriesLeft) {

		try {
			listenSocket = new ServerSocket(sendPort);

		} catch (IOException e) {

			// Tries the next port in a iterative way until it finds one that is available
			if(--noOfTriesLeft > 0 && ++sendPort <= 65535) {
				initListenSocket(noOfTriesLeft);
			}
			else {
				observers.sendConnectionWarning("Could not initiate a local sync!");
				resetLocalThreads = true;
				e.printStackTrace();
			}
		}
	}

	// Starts a thread and calls the needed methods in a loop
	private void startLocalSync() {

		localThread2 = new Thread(new Runnable() {
			public void run() {

				try {
					if(!resetLocalThreads) {
						observers.sendConnectionMessage("Connection: Local sync through port: " + sendPort);
						secondSocket = listenSocket.accept();

						BufferedInputStream inBufStream2 = new BufferedInputStream(secondSocket.getInputStream());
						BufferedOutputStream outBufStream2 = new BufferedOutputStream(secondSocket.getOutputStream());
						secondFolderStart(inBufStream2, outBufStream2);
						outBufStream2.close();
						inBufStream2.close();						
					}
				} catch (IOException e) {
					resetLocalThreads = true;
					e.printStackTrace();
				} 
			}
		});

		localThread1 = new Thread(new Runnable() {
			public void run() {

				try {
					if(!resetLocalThreads) {
						stateSocket = new Socket("localhost", sendPort);

						BufferedInputStream inBufStream = new BufferedInputStream(stateSocket.getInputStream());
						BufferedOutputStream outBufStream = new BufferedOutputStream(stateSocket.getOutputStream());
						activeStart(inBufStream, outBufStream);
						outBufStream.close();
						inBufStream.close();
					}
				} catch (UnknownHostException e) {
					resetLocalThreads = true;
					e.printStackTrace();
				} catch (IOException e) {
					resetLocalThreads = true;
					e.printStackTrace();
				}

				if(!resetLocalThreads) connectionMessage(true);

				restoreStateController();
			}
		});

		localThread2.start();
		localThread1.start();
	}

	// Restores the stateController after a local sync
	private void restoreStateController() {

		// Wait for localThread2 to finish
		while(localThread2.isAlive()) {
			localThread2.setPriority(Thread.MAX_PRIORITY);
			if(DEBUG) System.out.println("localThread2 is still running!");
			Thread.yield();
		}
		localThread2.setPriority(Thread.NORM_PRIORITY);

		// Restore the state before we started the localSync
		closeSockets(); // Closes the socket when everything is finished
		resetThreadVariables();
		observers.enableGuiComponents(true); // Unlocks all GUI components
		runningLocalSync = false;
		if(startListen) {
			startStateController();
		}
	}

}
